Source code for qmatchatea.utils.backend

# This code is part of qmatchatea.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.

"""
Backend of the simulation controlling:

- Precision
- Device
- Tensor network ansatz
- Settings for the MPI (if any)
- Tensor class used in the simulation
"""
import importlib
import json
import os
from typing import OrderedDict

from qtealeaves.tensors.tensor import GPU_AVAILABLE

from .mpi_utils import MPISettings

__all__ = ["QCBackend"]

# pylint: disable=too-many-arguments


[docs] class QCBackend: """ Backend for the simulation. Contains all the informations about which executable you want to run. Parameters ---------- precision: str, optional Precision of the simulation. Select a real precision ONLY if you use ONLY real gates. Available: - "A": automatic. For the heuristic see `self.resolve_precision`. - "Z": double precision complex; - "C": single precision complex; - "D": double precision real; - "S": single precision real. Default to "A". device: str, optional Device of the simulation. Available: - "A" : automatic. For the heuristic see `self.resolve_device` - "cpu": use the cpu - "gpu": use the gpu if it is available - "cpu+gpu": use the device-mixed mode (if gpu is available) Default to "A". ansatz : str, optional Whether to run the circuit with MPS or TTN tensor network ansatz. Default to "MPS". mpi_settings : MPISettings | None, optional Settings for running the simulation multi-node. Default to None, i.e. no MPI. tensor_module : str, optional Module used to perform the computations. Available: - `"numpy"` (Default, uses cupy for the GPU) - `"torch"`, uses pytorch; - `"tensorflow"` - `"jax"`. Default to `"numpy"` """ def __init__( self, precision="A", device="cpu", ansatz="MPS", mpi_settings=None, tensor_module="numpy", ): self._precision = precision.upper() self._device = device self._ansatz = ansatz.upper() self.mpi_settings = MPISettings() if mpi_settings is None else mpi_settings self.tensor_module = tensor_module.lower() if importlib.util.find_spec(self.tensor_module) is None: raise ValueError(f"Module {self.tensor_module} is not installed.")
[docs] def to_dict(self): """ Map the backend to a dictionary. """ dictionary = OrderedDict({}) mpi = "T" if self.num_procs > 1 else "F" gpu = "T" if self.device == "gpu" else "F" dictionary["simulation_mode"] = self.precision + mpi + gpu dictionary["approach"] = self.mpi_approach return dictionary
[docs] def resolve_precision(self, min_fidelity, tol=1e-7): """ Resolve the precision of the simulation. Heuristic if `self._precision="A"`. Parameters ---------- min_fidelity: float Lower bound of the fidelity of the simulation at the moment tol: float, optional Tolerance after which you switch to single precision. Default to 1e-7 Returns ------- str The selected precision """ if self._precision != "A": return self.precision # The lower bound of the fidelity of our state is # below the number of digits of a single # precision if 1 - min_fidelity > tol: return "C" return "Z"
[docs] def resolve_device(self, bond_dimension, previous_device, exp_gpu=7): """ Resolve the device if it set on automatic. Parameters ---------- bond_dimension : int Maximum bond dimension of the system previous_device : str Device where the system is currently. This is used to ensure we do not keep exchanging data back and forth. exp_gpu : int, optional Exponent of the bond dimension after which you switch to the gpu, i.e: - if chi >= 2**exp_gpu -> use gpu - if chi <= 2**(exp_gpu-1) -> use cpu Default to 7. (switch at 128) Returns ------- str Device where to move (or keep) the system """ if self._device != "A": return self._device if bond_dimension >= 2**exp_gpu and GPU_AVAILABLE: return "gpu" # The condition on the CPU is here because we want # to avoid keep exchanging informations if the bond # dimension oscillates between 129 and 120 if bond_dimension <= 2 ** (exp_gpu - 1): return "cpu" return previous_device
@property def where_barriers(self): """ This parameter is important only if you want to use MPI parallelization, where a barrier is equivalent to a canonization in the MPS simulation. Default to -1. """ return self.mpi_settings.where_barriers @property def precision(self): """Precision property""" return self._precision @property def device(self): """Device property""" return self._device @property def num_procs(self): """Number of processes property""" return self.mpi_settings.num_procs @property def mpi_approach(self): """mpi_approach property (inherited from `mpi_settings`)""" return self.mpi_settings.mpi_approach.upper() @property def ansatz(self): """ansatz property""" return self._ansatz @property def mpi_command(self): """mpi_command property""" return self.mpi_settings.mpi_command @property def identifier(self): """Identifier combining all properties.""" return ":".join( [ self.resolve_precision(1), self.resolve_device(1, "cpu"), str(self.num_procs), self.mpi_approach, self.ansatz, ] )
[docs] def to_json(self, path): """ Write the class as a json on file as backend.json in the folder path """ path = os.path.join(path, "backend.json") dictionary = OrderedDict() dictionary["device"] = self.device dictionary["precision"] = self.precision dictionary["num_procs"] = self.num_procs dictionary["ansatz"] = self.ansatz dictionary["mpi_approach"] = self.mpi_approach dictionary["mpi_command"] = self.mpi_command with open(path, "w") as fhandle: json.dump(dictionary, fhandle, indent=4)
[docs] @classmethod def from_json(cls, path): """ Initialize the class from a json file called "backend.json" in the folder path """ path = os.path.join(path, "backend.json") with open(path, "r") as fhandle: dictionary = json.load(fhandle)[0] return cls(**dictionary)